今天要介紹的是常用的 hook,useEffect。
React 元件本身是純函式,但還是有要處理到 side effect 的時候,而若要處理各種針對渲染出來的網頁後進行操作產生影響的 side effect,就設計出了 useEffect 去處理。
資料 fetch、事件監聽、setInterval、clearInterval、或手動改變 React component 中的 DOM 都是 side effect 的範例。
useEffect(() => {
// do ssomething...
}, [])
useEffect 第一個參數是一個 callback function,第二個參數是一個 dependency array,記錄 useEffect 內 callback function 用到的相關狀態、函式。
答案是透過 Object.is(),原始型別判斷值是否相同,物件型別判斷其參考是否相同。
如果物件型別使用 mutate 的方式更新,state 的 reference 相同就不會觸發重新渲染,這也是更新 state 要維持 Immutable 的原因之一。
const { someProperty } = someObject;
// good
useEffect(() => {
// 和 someProperty 相關的程式碼
}, [someObject.someProperty]);
// bad
useEffect(() => {
// 和 someProperty 相關的程式碼
}, [someObject]);
在 useEffect 中處理 side effect,有些需要做一些後續的處理,否則會有效能問題,有些則不用。
包含呼叫 api、DOM 操作等
清除方式就是回傳一個函式,是用來清理副作用的函式(cleanup),例如清除 timeout、取消一些訂閱、http 請求,去避免 memory leak 的問題。會在每次元件 下一次執行 effect 前 會被執行,用來清除前一次渲染所做的 side effect。
也就是說觸發 cleanup 的時機用步驟來說明的話,會如以下的樣子:
Ref:
Synchronizing with Effects
A Complete Guide to useEffect
// First render, props are {id: 10}
function Example() {
// ...
useEffect(
// Effect from first render
() => {
ChatAPI.subscribeToFriendStatus(10, handleStatusChange);
// Cleanup for effect from first render
return () => {
ChatAPI.unsubscribeFromFriendStatus(10, handleStatusChange);
};
}
);
// ...
}
// Next render, props are {id: 20}
function Example() {
// ...
useEffect(
// Effect from second render
() => {
ChatAPI.subscribeToFriendStatus(20, handleStatusChange);
// Cleanup for effect from second render
return () => {
ChatAPI.unsubscribeFromFriendStatus(20, handleStatusChange);
};
}
);
// ...
}
事件監聽(addEventListener、removeEventListener)、setTimeout、setInterval、觸發的動畫等...
以下面的範例為例,若沒有加上 return () => clearInterval(interval);
就點擊 Unmount child component 會出現錯誤訊息
App.jsx
import { useState } from "react";
import Counter from "./Counter.js";
export default function App() {
const [unmount, setUnmount] = useState(false);
const renderDemo = () => !unmount && <Counter />;
return (
<div>
<button onClick={() => setUnmount(true)}>Unmount child component</button>
{renderDemo()}
</div>
);
}
Counter.jsx
import { useState, useEffect } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(function () {
setCount((prev) => prev + 1);
}, 1000);
// 這裡必須做 clearInterval 的動作,否則會出現錯誤訊息
return () => clearInterval(interval);
}, []);
return <p>and the counter counts {count}</p>;
};
export default Counter;
將更新的樣式做復原
useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // Trigger the animation
return () => {
node.style.opacity = 0; // Reset to the initial value
};
}, []);
在 useEffect 內呼叫 api 時,也可以透過一個 Boolean flag 去控制,避免 api 發生 race conditions。
export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
}
}, [person]);
return (
<>
<select value={person} onChange={e => {
setPerson(e.target.value);
}}>
<option value="Alice">Alice</option>
<option value="Bob">Bob</option>
<option value="Taylor">Taylor</option>
</select>
<hr />
<p><i>{bio ?? 'Loading...'}</i></p>
</>
);
}
這段改寫自 React 官方文件-You Might Not Need an Effect 舉出幾個可以不用 useEffect 的情境,我自己看完也覺得很有幫助,所以整理成自己解讀的方式分享給讀者。
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// 可以不用在 props 更新時去重置 state
useEffect(() => {
setComment('');
}, [userId]);
// ...
}
上面這段看的出來是例如有一個檔案頁面 ProfilePage,當切到別人的檔案頁面時,要去重置顯示的評論,但其實這類 id/key 值的東西,可以透過 key prop 屬性解決。
export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}
function Profile({ userId }) {
// 根據 key 的特性去自動重置 state 的值
const [comment, setComment] = useState('');
}
怕讀者不明白 key 在這裡的作用,所以這裡也附上 React 官方的文件和一個範例。
這裡透過給兩個 Counter 個別的 key,當 isPlayerA 去做切換時,會根據 key 值做判定是否不同 key,是的話會將其中一個 Counter 從 DOM 移除,再呈現另外一個,就可以去重置被移除元件的 state。
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
Next player!
</button>
</div>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{person}'s score: {score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
如以下範例,當事件觸發後更新 product,並透過 useEffect 重新呼叫去跳出提示訊息,這種情況也可以不用用 useEffect。而且這樣寫每次 product 被更新都會跳出提示,產生 bug。
function ProductPage({ product, addToCart }) {
// 避免和事件處理相關的邏輯
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to the shopping cart!`);
}
}, [product]);
function handleBuyClick() {
addToCart(product);
}
function handleCheckoutClick() {
addToCart(product);
navigateTo('/checkout');
}
}
改善的做法是將事件處理的邏輯封裝在函式,那的確根本就用不到 useEffect。
function ProductPage({ product, addToCart }) {
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to the shopping cart!`);
}
function handleBuyClick() {
buyProduct();
}
function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
}
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// 沒問題的範例,是在元件渲染時呼叫的 api
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);
// 不好的範例,因為是透過事件更新 state 去觸發 useEffect
const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register', jsonToSubmit);
}
}, [jsonToSubmit]);
function handleSubmit(e) {
e.preventDefault();
setJsonToSubmit({ firstName, lastName });
}
}
改良:
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
function handleSubmit(e) {
e.preventDefault();
// 直接在函式裡呼叫 api
post('/api/register', { firstName, lastName });
}
}